package com.hww.common.util.crypto;

import com.hww.common.util.StringUtils;
import com.hww.common.util.base64.Base64;

import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @Author: hewenwen
 * @Description: AES算法封装
 * @Date: Created in 9:46 2022/2/22
 */
public class AESUtil {

    /**
     * 加密秘钥
     */
    private SecretKey secretKey;

    /**
     * 填充方式{@link Padding}
     */
    private String padding;

    /**
     * 偏移量iv
     * ECB模式下不需要设置iv偏移量
     * 其他模式下必须设置iv偏移量（可以将iv偏移量设置跟password一致）
     */
    private byte[] iv = null;

    private final Lock lock = new ReentrantLock();

    private Cipher cipher = null;

    /**
     * 构造
     *
     * @param mode    模式{@link Mode}
     * @param padding {@link Padding}补码方式
     * @param key     密钥，支持三种密钥长度：128、192、256位
     * @param iv      偏移向量，加盐  (可以与key一样的值)
     */
    public AESUtil(String mode, String padding, byte[] key, byte[] iv) {
        try {
            /*String modeName = mode.name();*/
            secretKey = new SecretKeySpec(key, "AES");
            this.padding = padding;
            String al = String.format("AES/%s/%s", mode, padding);
            if (!Mode.ECB.name().equals(mode)) {
                /*ECB加密模式，不需要设置iv偏移量*/
                this.iv = iv;
            }
            /*检查是否为ZeroPadding，是则替换为NoPadding，并标记以便单独处理*/
            if (Padding.ZeroPadding.name().equals(padding)) {
                al = al.replace(Padding.ZeroPadding.name(), Padding.NoPadding.name());
            }
            cipher = Cipher.getInstance(al);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加密
     *
     * @param data    被加密的字符串
     * @param charset 编码
     * @return 加密后的base64
     */
    public String encryptBase64(String data, Charset charset) {
        return Base64.encode(encrypt(StringUtils.bytes(data, charset)));
    }

    /**
     * 解密Base64表示的字符串
     *
     * @param data    被解密的String
     * @param charset 解密后字符的编码
     * @return 解密后的String
     */
    public String decryptStr(String data, Charset charset) {
        /*正则表达式，判断是否为16进制字符串*/
//        Pattern pattern = Pattern.compile("^[a-fA-F0-9]+$");
//        boolean isHex = pattern.matcher(data).matches();
//        if (isHex) {} else {}
        byte[] decrypt = decrypt(Base64.decode(data));
        return StringUtils.str(decrypt, charset);
    }




    /*-------------------------------------原始加解密-------------------------------------------*/

    /**
     * 加密
     *
     * @param data 被加密的bytes
     * @return 加密后的bytes
     */
    public byte[] encrypt(byte[] data) {
        lock.lock();
        try {
            final Cipher cipher = initCipher(Cipher.ENCRYPT_MODE);

            return cipher.doFinal(paddingDataWithZero(data, cipher.getBlockSize()));
        } catch (Exception e) {
            return null;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 解密
     *
     * @param bytes 被解密的bytes
     * @return 解密后的bytes
     */
    public byte[] decrypt(byte[] bytes) {
        final int blockSize;
        final byte[] decryptData;

        lock.lock();
        try {
            final Cipher cipher = initCipher(Cipher.DECRYPT_MODE);

            blockSize = cipher.getBlockSize();
            decryptData = cipher.doFinal(bytes);
        } catch (Exception e) {
            return null;
        } finally {
            lock.unlock();
        }

        return removePadding(decryptData, blockSize);
    }


    /**
     * 初始化{@link Cipher}为加密或者解密模式
     *
     * @param mode 模式，见{@link Cipher#ENCRYPT_MODE} 或 {@link Cipher#DECRYPT_MODE}
     * @return {@link Cipher}
     * @throws InvalidKeyException                无效key
     * @throws InvalidAlgorithmParameterException 无效算法
     */
    private Cipher initCipher(int mode) throws InvalidKeyException, InvalidAlgorithmParameterException {
        final Cipher cipher = this.cipher;
        if (null == this.iv || this.iv.length == 0) {
            cipher.init(mode, secretKey);
        } else {
            IvParameterSpec parameterSpec = new IvParameterSpec(this.iv);
            cipher.init(mode, secretKey, parameterSpec);
        }
        return cipher;
    }

    /*-------------------------------------原始加解密-------------------------------------------*/


    /**
     * 数据按照blockSize的整数倍长度填充填充0
     *
     * <p>
     * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * <p>
     * 见：https://blog.csdn.net/OrangeJack/article/details/82913804
     *
     * @param data      数据
     * @param blockSize 块大小
     * @return 填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     * @since 4.6.7
     */
    private byte[] paddingDataWithZero(byte[] data, int blockSize) {
        if (padding == Padding.ZeroPadding.name()) {
            final int length = data.length;
            // 按照块拆分后的数据中多余的数据
            final int remainLength = length % blockSize;
            if (remainLength > 0) {
                // 新长度为blockSize的整数倍，多余部分填充0
                return resize(data, length + blockSize - remainLength);
            }
        }
        return data;
    }


    /**
     * 数据按照blockSize去除填充部分，用于解密
     *
     * <p>
     * 在{@link Padding#ZeroPadding} 模式下，且数据长度不是blockSize的整数倍才有效，否则返回原数据
     *
     * @param data      数据
     * @param blockSize 块大小，必须大于0
     * @return 去除填充后的数据，如果isZeroPadding为false或长度刚好，返回原数据
     * @since 4.6.7
     */
    private byte[] removePadding(byte[] data, int blockSize) {
        if (padding == Padding.ZeroPadding.name() && blockSize > 0) {
            final int length = data.length;
            final int remainLength = length % blockSize;
            if (remainLength == 0) {
                // 解码后的数据正好是块大小的整数倍，说明可能存在补0的情况，去掉末尾所有的0
                int i = length - 1;
                while (i >= 0 && 0 == data[i]) {
                    i--;
                }
                return resize(data, i + 1);
            }
        }
        return data;
    }


    /**
     * 生成一个新的重新设置大小的数组<br>
     * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置，其它位置补充0，缩小则截断
     *
     * @param bytes   原数组
     * @param newSize 新的数组大小
     * @return 调整后的新数组
     */
    private byte[] resize(byte[] bytes, int newSize) {
        if (newSize < 0) {
            return bytes;
        }
        final byte[] newArray = new byte[newSize];
        if (newSize > 0 && null != bytes && bytes.length > 0) {
            System.arraycopy(bytes, 0, newArray, 0, Math.min(bytes.length, newSize));
        }
        return newArray;
    }


/*    public static void main(String[] args) {
        Charset charset = StandardCharsets.UTF_8;
        byte[] password = "EFA509031BA8BD6C".getBytes(charset);
        String a = "12/*34@#56";
        AESUtil aesUtil = new AESUtil(Mode.CBC.name(), Padding.ZeroPadding.name(), password, password);
        String s = aesUtil.encryptBase64(a, charset);
        System.out.println("---加密:" + s);
        String s1 = aesUtil.decryptStr(s, charset);
        System.out.println("---解密:" + s1);
    }*/

}